A detailed backend reference

Error Handling & Sistem Tahan Gagal

Error bukan pengecualian, ia keniscayaan. Query database gagal, API eksternal timeout, pengguna mengirim data buruk, dan logika bisnis kena kasus tepi yang tidak diprediksi siapa pun. Bab ini membangun pola pikir dan perlengkapan konkret untuk mendeteksi, menangani, dan memulihkan diri dari setiap kelas error backend, sebelum diam-diam menguras uang atau kepercayaan Anda.

01

Pola Pikir Tahan Gagal

"Pertanyaannya bukan apakah error akan terjadi, tapi bagaimana Anda menanganinya saat terjadi."

Setiap engineer backend harus menginternalisasi kebenaran sederhana: sistem Anda akan gagal. Bukan mungkin. Akan. Sumbernya ada di mana-mana:

  • Query database kadang gagal atau timeout.
  • API eksternal (pembayaran, email, auth) akan mati.
  • Pengguna akan mengirim data salah bentuk, hilang, atau jahat.
  • Logika bisnis akan kena kasus tepi yang tidak terpikirkan siapa pun saat desain.

Sistem tahan gagal bukan yang tidak pernah rusak. Ia yang rusak secara bisa diprediksi, pulih dengan anggun, dan memberi tahu Anda persis apa yang terjadi. Mencapainya butuh pergeseran pola pikir yang disengaja, dari "nanti kuhandle error" menjadi "kudesisain untuk kegagalan sejak hari pertama".

The best error handling starts before the error happens. Proactive detection, health checks, and validation prevent most runtime surprises.
02

Lima Kelas Error Backend

Error backend bisa dikelompokkan ke lima kategori luas. Masing-masing punya asal, strategi deteksi, dan perbaikan berbeda.

(1) Logic Errors

Aplikasi berjalan tapi menghasilkan hasil yang salah. Paling sulit dideteksi. Bisa diam-diam menguras uang selama berminggu-minggu.

(2) Database Errors

Kegagalan koneksi, deadlock, pelanggaran constraint, SQL salah bentuk. Bisa merobohkan seluruh aplikasi.

(3) External Service Errors

API pihak ketiga (pembayaran, email, auth) timeout, kena rate-limit, atau offline. Anda tidak punya kendali.

(4) Input Validation Errors

Pengguna mengirim data buruk, hilang, atau di luar rentang. Paling mudah ditangani, jika lapisan validasi Anda kokoh.

(5) Configuration Errors

Env var hilang, kredensial salah saat deploy. Muncul saat startup, bukan runtime, jika Anda melakukannya dengan benar.

03

Logic Error: Pembunuh Senyap

Logic error adalah kelas paling berbahaya karena aplikasi Anda terus berjalan, ia hanya melakukan hal yang salah. Tanpa crash, tanpa stack trace, tanpa respons 500. Hanya hasil yang diam-diam salah yang menumpuk seiring waktu.

Contoh Klasik

Platform e-commerce menerapkan diskon dua kali karena bug di mesin promosi. Hasilnya: biaya pengiriman negatif. Aplikasi berjalan sempurna. Setiap order terkirim. Perusahaan rugi di setiap transaksi. Ini tak disadari berminggu-minggu karena tidak ada alert monitoring yang menyala pada "biaya pengiriman negatif".

Akar Penyebab yang Umum

  • Kebutuhan yang disalahpahami: catatan dari rapat sprint ambigu; Anda mengimplementasikan apa yang Anda kira diminta, bukan yang dimaksud.
  • Algoritma salah: formula diskon atau harga kompleks punya off-by-one atau operator salah (* alih-alih +).
  • Kasus tepi tak tertangani: pengguna yang belum pernah membeli memicu jalur diskon "berbasis pembelian lampau" yang tidak dirancang untuk pengguna nol pembelian.
Logic errors involving money, permissions, or security can corrupt data and produce wrong business results for months without a single error log entry. They are only found through careful testing, monitoring business metrics, and code review.

Strategi Pencegahan

  • Tulis unit test untuk setiap aturan bisnis, terutama logika diskon, harga, dan izin.
  • Tambahkan monitoring metrik bisnis (mis. alert jika rata-rata nilai order turun >20% dalam satu jam).
  • Pakai property-based testing (Go: gopter, Python: hypothesis) untuk membuat input kasus tepi secara otomatis.
  • Wajibkan peer review untuk semua perubahan kode terkait pembayaran dan auth.
04

Error Database

Kebanyakan aplikasi backend tak berarti tanpa database-nya. Error database jenis apa pun berarti aplikasi Anda tidak bisa menyajikan data nyata, yang biasanya berarti UI rusak atau kegagalan berantai lintas layanan.

(1) Error Koneksi

Backend Anda tidak bisa menjangkau server database. Penyebab yang mungkin:

  • Partisi jaringan atau kegagalan DNS antara server aplikasi dan server DB.
  • Server database kelebihan beban atau mati.
  • Connection pool habis: semua koneksi TCP yang dipool sedang dipakai; request baru mengantre atau langsung gagal.
Connection pooling keeps a fixed number of open TCP connections to the database, avoiding the overhead of a full TCP handshake + TLS negotiation on every query. Tools: pgxpool (Go), psycopg2 pool or SQLAlchemy (Python), pg Pool (Node). Size your pool carefully: too small -> bottleneck; too large -> DB overload.

(2) Error Pelanggaran Constraint

Anda mencoba melakukan operasi yang melanggar aturan tingkat database:

Tipe ConstraintPemicuRespons yang Tepat
UniqueSisipkan email / username duplikatHTTP 409 Conflict atau 400, "Email sudah dipakai"
Foreign KeyMereferensikan baris yang tidak adaHTTP 404, "Author ID tidak ditemukan" / 400
Not NullNilai kolom wajib hilangHTTP 400, "Field X wajib diisi"
CheckNilai gagal aturan kustom (mis. harga > 0)HTTP 400, pesan spesifik domain

(3) Error Query / Sintaks

SQL salah bentuk, typo nama tabel, mereferensikan kolom yang diganti nama, atau kondisi join yang hilang. Ini biasanya tertangkap di pengembangan tapi bisa lolos jika string SQL mentah dibangun secara dinamis.

Never construct SQL by string concatenation with user input. Use parameterised queries / prepared statements always. This prevents both query errors and SQL injection attacks.

(4) Deadlock

Deadlock terjadi saat dua (atau lebih) transaksi masing-masing memegang kunci yang dibutuhkan yang lain:

Transaction A holds LOCK on Row 1 Transaction B holds LOCK on Row 2 A wants Row 2 -> <- B wants Row 1 DEADLOCK circular wait, DB kills one Tx
Fig 1: Deadlock: two transactions waiting on each other's locks indefinitely. The DB detects and aborts one.

Postgres mendeteksi deadlock otomatis dan mematikan satu transaksi dengan kode error 40P01. Aplikasi Anda harus me-retry transaksi itu. Pencegahan: selalu ambil kunci dalam urutan yang konsisten di semua jalur kode.

05

Error Layanan Eksternal

Backend SaaS modern bergantung pada konstelasi layanan pihak ketiga, pemroses pembayaran (Stripe), email (Resend, SendGrid), object storage (S3), auth (Clerk, Auth0), AI (OpenAI). Setiap satu dari ini adalah titik kegagalan di luar kendali Anda.

Your Backend HTTP / TCP / WS Stripe Resend Clerk/Auth0 S3 Storage OpenAI Analytics Any of these going down = your problem
Fig 2: Each external dependency is an independent failure point your app must handle gracefully.

(1) Kegagalan Jaringan

Internet antara server Anda dan API eksternal tidak andal. Anda akan menjumpai: connection timeout, kegagalan resolusi DNS, partisi jaringan, dan error handshake TLS. Set timeout eksplisit pada setiap panggilan HTTP keluar, jangan pernah biarkan API pihak ketiga yang lambat memblokir goroutine / thread Anda tanpa batas.

(2) Rate Limiting: HTTP 429

Setiap API eksternal serius menegakkan rate limit untuk mencegah penyalahgunaan. Jika aplikasi Anda menghantam API (karena bug, lonjakan lalu lintas, atau error loop), Anda akan menerima HTTP 429 Too Many Requests.

Mitigasi standarnya Exponential Backoff dengan Jitter:

Try #1 -> 429 1 s Try #2 -> 429 2 s Try #3 -> 429 4 s Try #4 -> 429 8 s Try #5 -> 200 Wait = base x 2^n + random_jitter, doubles each retry, jitter prevents thundering herd
Fig 3: Exponential Backoff: each retry waits 2x longer. Jitter spreads retries to avoid thundering herd.

(3) Gangguan Layanan / Downtime

Penyedia cloud besar (AWS, GCP) dan layanan SaaS populer kadang mati. Aplikasi Anda butuh strategi saat dependensi kritis sepenuhnya tidak tersedia:

  • Fallback: jika cache Redis mati, jatuh ke pembacaan DB langsung untuk data non-kritis.
  • Degradasi anggun: nonaktifkan fitur yang terdampak (mis. "saran AI untuk sementara tidak tersedia") alih-alih memecahkan seluruh aplikasi.
  • Pola circuit breaker: setelah N kegagalan beruntun, berhenti mengirim request ke layanan yang rusak dan kembalikan respons cache/default seketika. Coba lagi layanannya setelah periode pendinginan.
06

Error Validasi Input

Ini error paling mudah ditangani karena Anda yang mendefinisikan aturannya. Lapisan validasi Anda adalah garis pertahanan pertama: tangkap data buruk di titik masuk, sebelum ia mencapai database atau logika bisnis Anda.

Jenis Validasi

TipeYang DiperiksaContoh
FormatBentuk/pola nilaiRegex email, tanggal ISO, telepon E.164
RentangBatas numerik, panjang string, ukuran larikHarga: 0-99999, nama: 2-100 karakter
WajibField wajib adauser_id tidak boleh null
Aturan BisnisBatasan spesifik domainenddate booking > startdate
ReferensialEntitas terkait benar-benar adacategory_id ada di tabel categories

Selalu validasi di kedua lapisan: frontend (UX) dan backend (keamanan). Jangan pernah percaya validasi sisi klien sendirian. Backend adalah gerbang yang otoritatif.

Return HTTP 400 Bad Request with a structured error body listing every field that failed and why. Don't return a single generic message, help the user fix all their mistakes in one round-trip.
07

Error Konfigurasi

Error konfigurasi terjadi di batas antar lingkungan, dev -> staging -> produksi. OPENAIAPIKEY yang hilang, URL database yang salah, atau secret yang terlupakan bisa diam-diam memecah fitur spesifik sementara sisa aplikasi tampak sehat.

Gagal Cepat saat Startup: Bukan saat Runtime

Aturan emasnya: validasi semua environment variable wajib sebelum server mulai menerima lalu lintas. Jika ada yang hilang atau korup, crash seketika dengan pesan error yang jelas.

Buruk: Kegagalan Runtime

  • Aplikasi mulai sukses
  • Pengguna pertama memukul endpoint gambar AI
  • Panggilan OpenAI gagal, key-nya hilang
  • Pengguna dapat error 500 yang misterius
  • Deployment lama sudah dihentikan
  • Situs mati sampai diperbaiki manual

Baik: Kegagalan Startup

  • Deployment baru mulai
  • Validasi konfigurasi berjalan seketika
  • Key hilang terdeteksi -> proses keluar dengan pesan jelas
  • Blue-green: deployment lama masih berjalan
  • Nol downtime, tim ops memperbaiki dan deploy ulang
Blue-green deployments make startup-time crashes safe: the new version must pass health checks before the old version is terminated. If the new version crashes at start, the old version keeps serving traffic.

Go: Validasi Konfigurasi saat Boot

Gopackage config

import (
    "fmt"
    "os"
    "strings"
)

type Config struct {
    DatabaseURL    string
    OpenAIKey      string
    JWTSecret      string
    ResendAPIKey   string
}

// MustLoad panics if any required variable is missing.
// Call this once in main() before http.ListenAndServe.
func MustLoad() Config {
    required := []string{
        "DATABASE_URL",
        "OPENAI_API_KEY",
        "JWT_SECRET",
        "RESEND_API_KEY",
    }

    var missing []string
    for _, key := range required {
        if os.Getenv(key) == "" {
            missing = append(missing, key)
        }
    }
    if len(missing) > 0 {
        // Crash immediately, loud and clear
        panic(fmt.Sprintf("[FATAL] missing required env vars: %s",
            strings.Join(missing, ", ")))
    }

    return Config{
        DatabaseURL:  os.Getenv("DATABASE_URL"),
        OpenAIKey:    os.Getenv("OPENAI_API_KEY"),
        JWTSecret:    os.Getenv("JWT_SECRET"),
        ResendAPIKey: os.Getenv("RESEND_API_KEY"),
    }
}

// main.go
func main() {
    cfg := config.MustLoad()  // panics here if config invalid
    server := newServer(cfg)
    log.Fatal(server.ListenAndServe())
}
08

Deteksi Error Proaktif: Health Check

"Penanganan error terbaik dimulai sebelum error terjadi."

Health check terus-menerus memverifikasi bahwa sistem Anda bekerja, bukan hanya bahwa ia berjalan. Ada perbedaan kritis:

Basic Liveness Check GET /health -> 200 OK Checks: is the process running? Does NOT verify DB / cache / external Deep Readiness Check GET /ready -> 200 OK Runs test query, pings cache, verifies all subsystems are functional
Fig 4: Liveness vs Readiness: the industry-standard split (Kubernetes uses both).

Apa yang Diperiksa

  • Database: jalankan query ringan yang representatif. Lacak waktu query; jika melonjak dari 50ms ke 4 detik, ada yang salah sebelum pengguna menyadarinya.
  • Layanan eksternal: pemroses pembayaran: jalankan transaksi uji berkala; email: kirim ke alamat internal; auth: buat dan validasi token uji.
  • Konfigurasi: verifikasi semua env var wajib dimuat dan tidak kosong saat startup.
  • Pemanasan cache: pastikan cache kritis (session store, katalog produk) terisi sebelum melayani lalu lintas.

Go: Deep Health Check Endpoint

Gotype HealthStatus struct {
    Status   string            `json:"status"`
    Checks   map[string]string `json:"checks"`
}

func healthHandler(db *pgxpool.Pool, rdb *redis.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        checks := map[string]string{}
        overall := "ok"

        // DB check
        ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
        defer cancel()
        if err := db.Ping(ctx); err != nil {
            checks["database"] = "unhealthy: " + err.Error()
            overall = "degraded"
        } else {
            checks["database"] = "ok"
        }

        // Redis check
        if err := rdb.Ping(r.Context()).Err(); err != nil {
            checks["cache"] = "unhealthy: " + err.Error()
            overall = "degraded"
        } else {
            checks["cache"] = "ok"
        }

        status := http.StatusOK
        if overall != "ok" { status = http.StatusServiceUnavailable }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(status)
        json.NewEncoder(w).Encode(HealthStatus{Status: overall, Checks: checks})
    }
}
09

Monitoring & Observabilitas

Health check memberi tahu Anda ada yang rusak sekarang. Monitoring memberi tahu Anda ada yang hampir rusak, dan memberi konteks untuk memahami kenapa sesuatu rusak setelah kejadian.

Yang Dilacak

KategoriMetrik untuk DipantauKenapa
Lapisan HTTPTingkat 4xx / 5xx, latensi p50/p95/p99Memunculkan masalah yang terlihat pengguna secara langsung
DatabaseDurasi query, pemakaian connection pool, jumlah deadlockMendeteksi query lambat sebelum timeout
Layanan eksternalTingkat sukses panggilan, latensi, jumlah 429Tahu kapan dependensi sedang menurun
Metrik bisnisTransaksi sukses/menit, pembayaran gagal, tingkat pendaftaranMenangkap logic error yang tak terlihat dari tingkat error
InfrastrukturCPU, memori, I/O disk, throughput jaringanKehabisan resource mendahului crash
Don't only track error rates. A drop in successful transactions from 1000/min to 200/min is a critical problem, even if the error rate shows 0%. Always monitor positive business outcomes, not just failure signals.

Structured Logging (JSON)

Log teks polos sulit di-query di skala. Pakai log JSON terstruktur supaya tool agregasi log (Grafana Loki, Datadog, ELK) bisa mem-parse, memfilter, dan memberi alert padanya secara programatik.

Python / structlogimport structlog

log = structlog.get_logger()

# Good, structured, queryable, no sensitive data
log.error(
    "payment_failed",
    user_id="u_9a3f",          # ID, not email
    correlation_id="req_abc123",
    provider="stripe",
    error_code="card_declined",
    amount_cents=4999,
)

# BAD, never log PII or secrets
# log.error("payment_failed", email="alice@example.com", card="4242...")
10

Strategi Pemulihan

Dapat Dipulihkan vs Tidak

Error yang Dapat Dipulihkan

  • Gangguan jaringan sesaat ke API email
  • Connection pool database untuk sementara habis
  • Rate limit 429 dari layanan eksternal

Strategi: Retry dengan exponential backoff. Antrekan pekerjaannya. Jangan menyerah seketika.

Error yang Tidak Dapat Dipulihkan

  • Klaster Redis sepenuhnya mati
  • Pemroses pembayaran offline berjam-jam
  • Data korup di DB

Strategi: Degradasi anggun. Fallback. Nonaktifkan fiturnya. Lindungi fungsionalitas inti.

Exponential Backoff di Go

Gofunc sendEmailWithRetry(to, subject, body string) error {
    maxRetries := 5
    baseDelay  := 1 * time.Second

    for attempt := 0; attempt < maxRetries; attempt++ {
        err := emailClient.Send(to, subject, body)
        if err == nil {
            return nil  // success
        }

        if !isRetryable(err) {
            return fmt.Errorf("permanent failure: %w", err)
        }

        // Exponential backoff: 1s, 2s, 4s, 8s, 16s
        wait := baseDelay * time.Duration(1<<attempt)
        // Add jitter (+/-20%) to prevent thundering herd
        jitter := time.Duration(rand.Int63n(int64(wait / 5)))
        time.Sleep(wait + jitter)

        log.Warn("email send failed, retrying",
            "attempt", attempt+1,
            "wait_ms", wait.Milliseconds(),
            "error", err)
    }
    return fmt.Errorf("all %d retries exhausted", maxRetries)
}

func isRetryable(err error) bool {
    // Retry on 429, 503, network errors; not on 400, 401, 422
    var httpErr *HTTPError
    if errors.As(err, &httpErr) {
        return httpErr.StatusCode == 429 || httpErr.StatusCode >= 500
    }
    return true  // network errors are always retryable
}

Pemulihan Otomatis vs Manual

  • Otomatis: restart proses yang crash (systemd, kebijakan restart Kubernetes), bersihkan cache korup, pindah ke sistem cadangan. Rancang hati-hati, pemulihan otomatis kadang bisa memperbesar masalah.
  • Manual: korupsi data, selisih pembayaran, insiden keamanan. Ini butuh penilaian manusia. Dokumentasikan runbook-nya. Ujilah. Ketahui siapa yang on-call.
Data integrity is your #1 priority during any incident. Never auto-delete or auto-migrate data as part of an error recovery procedure. Take a backup first, always.
11

Global Error Handler: Jaring Pengaman Terakhir

Global error handler adalah satu middleware yang duduk di lapisan terluar aplikasi Anda, menangkap setiap error yang memuncak dari lapisan mana pun, dan mengubahnya menjadi respons HTTP yang diformat dengan benar.

Request Flow Router Handler -> ValidationError (400) Service -> BusinessError Repository -> DBError (unique, norows, fk) errors bubble up Global Error Handler Middleware ValidationError -> HTTP 400 + field errors DB UniqueViolation -> HTTP 409 Conflict DB NoRows -> HTTP 404 Not Found Unknown Error -> HTTP 500 + generic msg HTTP Response 400 Bad Request 409 Conflict 404 Not Found 500 Internal Error Safe, user-friendly messages only. No internal details.
Fig 5: All errors bubble up to one middleware. One place to define every response format.

Dua Keuntungan Utama

  • Tidak ada kondisi error yang terlupakan: setiap error tak tertangani jatuh ke kasus default handler global (500 + "something went wrong"). Tidak ada yang ditelan diam-diam.
  • Nol redundansi: logika penanganan error database tinggal di satu file, bukan tersebar di 40 method repository. Ubah pesan pelanggaran-unique sekali, ia berlaku di mana-mana.
12

Go: Implementasi Global Error Handler

Go tidak punya exception, error adalah nilai kembalian. Polanya mengembalikan error ke atas call stack dan menanganinya di middleware.

Go / errors/types.gopackage apperr

import "net/http"

// AppError is the canonical error type for this application.
type AppError struct {
    Code    int      // HTTP status code
    Message string   // Safe, user-facing message
    Details any      // Optional: field-level errors for 400s
    Err     error    // Original error, for logging only, NEVER sent to client
}

func (e *AppError) Error() string { return e.Message }

// Constructors
func NotFound(resource string) *AppError {
    return &AppError{Code: http.StatusNotFound, Message: resource + " not found"}
}
func Conflict(msg string) *AppError {
    return &AppError{Code: http.StatusConflict, Message: msg}
}
func BadRequest(msg string, details any) *AppError {
    return &AppError{Code: http.StatusBadRequest, Message: msg, Details: details}
}
func Internal(err error) *AppError {
    return &AppError{
        Code:    http.StatusInternalServerError,
        Message: "something went wrong",  // NEVER expose err.Error() here
        Err:     err,
    }
}
Go / middleware/error_handler.gopackage middleware

import (
    "encoding/json"
    "errors"
    "log/slog"
    "net/http"

    "github.com/jackc/pgx/v5/pgconn"
    apperr "yourapp/errors"
)

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details any    `json:"details,omitempty"`
}

// GlobalErrorHandler wraps a handler that returns an error.
func GlobalErrorHandler(next func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := next(w, r)
        if err == nil { return }

        var appErr *apperr.AppError

        switch {
        // Already wrapped as AppError
        case errors.As(err, &appErr):
            if appErr.Err != nil {
                slog.Error("app error", "err", appErr.Err)
            }

        // Postgres unique constraint violation -> 409
        case isPgError(err, "23505"):
            appErr = apperr.Conflict("resource already exists")

        // Postgres foreign key violation -> 404
        case isPgError(err, "23503"):
            appErr = apperr.NotFound("referenced resource")

        // pgx no-rows -> 404
        case errors.Is(err, pgx.ErrNoRows):
            appErr = apperr.NotFound("resource")

        // Everything else -> 500 (never leak internal error)
        default:
            slog.Error("unhandled error", "err", err)
            appErr = apperr.Internal(err)
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(appErr.Code)
        json.NewEncoder(w).Encode(ErrorResponse{
            Code:    appErr.Code,
            Message: appErr.Message,
            Details: appErr.Details,
        })
    }
}

func isPgError(err error, code string) bool {
    var pgErr *pgconn.PgError
    return errors.As(err, &pgErr) && pgErr.Code == code
}
13

Python: Global Error Handler (FastAPI)

Python / FastAPIfrom fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from psycopg2 import errors as pg_errors
import logging

app = FastAPI()
logger = logging.getLogger("app")

# --- Custom exception types ---
class AppError(Exception):
    def __init__(self, status: int, message: str, details=None):
        self.status  = status
        self.message = message
        self.details = details

class NotFoundError(AppError):
    def __init__(self, resource: str):
        super().__init__(404, f"{resource} not found")

class ConflictError(AppError):
    def __init__(self, msg: str):
        super().__init__(409, msg)

# --- Global exception handlers ---
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    return JSONResponse(
        status_code=exc.status,
        content={"code": exc.status, "message": exc.message, "details": exc.details}
    )

@app.exception_handler(pg_errors.UniqueViolation)
async def unique_violation_handler(request: Request, exc):
    logger.warning("unique_violation", extra={"path": request.url.path})
    return JSONResponse(status_code=409,
        content={"code": 409, "message": "resource already exists"})

@app.exception_handler(pg_errors.ForeignKeyViolation)
async def fk_violation_handler(request: Request, exc):
    return JSONResponse(status_code=404,
        content={"code": 404, "message": "referenced resource not found"})

@app.exception_handler(Exception)
async def unhandled_error_handler(request: Request, exc: Exception):
    # Log the real error internally, never expose it
    logger.error("unhandled_exception", exc_info=exc,
        extra={"path": request.url.path})
    return JSONResponse(status_code=500,
        content={"code": 500, "message": "something went wrong"})
14

Keamanan: Yang Diekspos, Yang Disembunyikan

Every error message that leaves your backend is a potential information leak. Treat your error responses with the same care as your API responses.

(1) Jangan Pernah Membocorkan Detail Internal

Pesan error database dari Postgres memuat nama tabel, nama kolom, nama index, dan nama constraint. Jika Anda meneruskan pesan pgconn.PgError mentah langsung ke klien, penyerang mempelajari skema Anda dan bisa menyusun percobaan SQL injection yang lebih tertarget.

Yang Anda DapatYang Dikirim ke Klien
duplicate key value violates unique constraint "usersemailkey""Email sudah dipakai"
relation "usres" does not exist (typo)"Ada yang salah"
deadlock detected on relation 42816"Ada yang salah, coba lagi"
stack trace: panic at server.go:142"Internal server error"

(2) Error Auth yang Samar (Disengaja)

Endpoint login adalah permukaan yang paling banyak diserang di aplikasi mana pun. Jika Anda mengembalikan pesan spesifik seperti "tidak ada user dengan email ini" vs "kata sandi salah", penyerang bisa menghitung email valid lewat loop sederhana.

Attacker's Strategy (when you use specific errors) Step 1: Email Enum Try many emails with fake pwd -> "user not found" until hit Step 2: Got Valid Email Now tries common passwords -> "wrong password" on each miss Step 3: Account Compromised Your user's account is now owned The Fix: Always use a generic message { "message": "invalid email or password" } <- same message always
Fig 6: Specific auth error messages enable email enumeration attacks. Use a single generic message for all auth failures.

(3) Praktik Logging yang Aman

Log sering dikirim ke layanan agregasi pihak ketiga (Datadog, Grafana Cloud, ELK). Dalam pelanggaran data besar, file log yang bocor mengekspos jutaan record, karena engineer ceroboh mencatat field sensitif.

  • Jangan pernah log: kata sandi, API key, nomor kartu kredit, SSN, alamat email lengkap, session token.
  • Log sebagai gantinya: user ID (bukan email), correlation/request ID, nama operasi, kode error.
  • Pakai library pembersih log (mis. Go: slog dengan handler kustom; Python: prosesor structlog) untuk meng-redact otomatis field sensitif yang dikenal.
Go / safe vs unsafe logging//UNSAFE, never do this
slog.Error("login_failed",
    "email", user.Email,          // PII leak
    "password", req.Password,     // catastrophic
    "api_key", cfg.OpenAIKey,      // secret leak
)

// SAFE, IDs and correlation only
slog.Error("login_failed",
    "user_id", user.ID,
    "correlation_id", r.Header.Get("X-Request-ID"),
    "reason", "invalid_credentials",  // generic code, not DB message
)
Follow the OWASP Authentication Cheat Sheet for authentication endpoint security. It covers error message handling, brute-force protection, account lockout, and more.
15

Referensi & Bacaan Lanjutan


Versi Sederhana

Backend Field Manual / Error Handling & Fault Tolerance / Bab 16